package de.hpi.generator.model.dependencies;

import de.hpi.generator.config.DependencyModel;
import de.hpi.generator.config.UserInputException;
import de.hpi.generator.generator.distributors.DataDistributor;
import de.hpi.generator.generator.storages.UniqueValuesDataStorage;
import de.hpi.generator.generator.storages.ValueSeriaInfo;
import de.hpi.generator.model.Column;
import de.hpi.generator.model.ColumnsCollection;
import de.hpi.generator.model.Database;
import de.hpi.generator.model.TesmaColumn;
import java.util.ArrayList;
import java.util.UUID;

public class CompositeInclusionDependency extends InclusionDependency {

    protected ColumnsCollection aDependentColumns = new ColumnsCollection(null);
    protected ColumnsCollection aReferencedColumns = new ColumnsCollection(null);
    protected DataDistributor aDistributor = null;
    private boolean isGenerated = false;

    public boolean isGenerated() {
        return isGenerated;
    }

    @Override
    public ColumnsCollection getDependentColumns() {
        return aDependentColumns;
    }

    @Override
    public ColumnsCollection getReferencedColumns() {
        return aReferencedColumns;
    }

    public DataDistributor getDistributor() {
        return this.aDistributor;
    }
    protected Database aOwner = null;

    public Database getOwner() {
        return this.aOwner;
    }
    protected String aName = null;

    public String getName() {
        return this.aName;
    }
    protected UniqueValuesDataStorage aUniqueValuesDataStorage = null;

    /**
     * Inclusion Dependency like (A,B)->(C,D)
     * @param aDependentColumnsCollection (A,B)
     * @param aReferencedColumnCollection (C,D)
     */
    public CompositeInclusionDependency(Database aOwner, DependencyModel aInclusionDependencyModel) throws Exception {
        super(null, null);

        this.aOwner = aOwner;
        this.aName = UUID.randomUUID().toString();
        this.aUniqueValuesDataStorage = new UniqueValuesDataStorage(getUniqueValuesDataFilePath());
        this.aUniqueValuesDataStorage.clean();
        this.aDistributor = DataDistributor.findDataDistributor(aInclusionDependencyModel.distribution);

        initInclusionDependency(aInclusionDependencyModel, aOwner);
    }

    private void initInclusionDependency(DependencyModel aInclusionDependencyModel, Database aOwner) throws Exception {
        String[] aDependentColumnNames = aInclusionDependencyModel.dependent.split(",");
        String[] aReferencedColumnNames = aInclusionDependencyModel.referenced.split(",");

        if (aDependentColumnNames.length != aReferencedColumnNames.length) {
            throw new UserInputException("The length of dependent part of composite Inclusion Dependency "
                    + "should be equal to the length of referenced part of composite Inclusion Dependency");
        }

        for (int i = 0; i < aReferencedColumnNames.length; i++) {
            Column aDependentColumn = aOwner.findColumnByPath(aDependentColumnNames[i]);
            aDependentColumns.add(aDependentColumn);

            Column aReferencedColumn = aOwner.findColumnByPath(aReferencedColumnNames[i]);
            validateColumn(aReferencedColumn);
            aReferencedColumns.add(aReferencedColumn);
        }

        checkForTesmaColumns(aDependentColumns);
    }

    private void checkForTesmaColumns(ColumnsCollection aDependentColumns) throws Exception {
        Column aTesmaColumn = findTesmaColumn(aDependentColumns);
        if (aTesmaColumn == null) {
            return;
        }

        if (findNonTesmaColumn(aDependentColumns) != null) {
            throw new Exception("Cannot mix dependent columns from tesma and datagenerator tables.");
        }

        String aTesmaTable = aTesmaColumn.getOwner().getName();

        for (Column aColumn : aDependentColumns) {
            if (aTesmaTable.compareTo(aColumn.getOwner().getName()) != 0) {
                throw new Exception("Cannot mix columns from different tables of tesma");
            }
        }
    }

    private Column findTesmaColumn(ColumnsCollection aColumns) {
        for (Column aColumn : aColumns) {
            if (isTesmaColumn(aColumn)) {
                return aColumn;
            }
        }
        return null;
    }

    private Column findNonTesmaColumn(ColumnsCollection aColumns) {
        for (Column aColumn : aColumns) {
            if (isTesmaColumn(aColumn) == false) {
                return aColumn;
            }
        }
        return null;
    }

    protected void validateColumn(Column aColumn) throws Exception {

        if (isTesmaColumn(aColumn)) {
            throw new Exception(String.format(
                    "Column [%s] cannot be as part of composite inclusion",
                    aColumn.toString()));
        }
    }

    private boolean isTesmaColumn(Column aColumn) {
        return aColumn.getDataType().compareTo(TesmaColumn.TYPE_NAME) == 0;
    }

    public String getUniqueValuesDataFilePath() {
        return this.getWorkingDirectoryPath() + "\\UniqueValues.bin";
    }

    public String getWorkingDirectoryPath() {
        return this.getOwner().getWorkingDirectoryPath() + "\\CD\\CD-" + this.getName();
    }

    @Override
    public void generateDependentData() throws Exception {
        generateCompositeInclusionDependency();
    }

    /**
     * Precondition all dependent columns are generated and have same length
     * All referenced columns are not generated yet, otherwise false distribution
     * @throws Exception 
     */
    protected void generateCompositeInclusionDependency() throws Exception {
        if (isGenerated) {
            return;
        }
        System.out.println("Generating composite inclusion dependency: " + this.toString());
        generateBaseDependentData();
        balanceDependentValues();
        generateReferencedData();
        aReferencedColumns.markAsGenerated();
        this.markAsGenerated();
    }

    private void markAsGenerated() {
        isGenerated = true;
    }

    protected void generateBaseDependentData() throws Exception {
        for (Column aColumn : aDependentColumns) {
            aColumn.generateBaseColumnValues();
        }
    }

    private void balanceDependentValues() throws Exception {
        int aMaxNumberOfDependentValues = aDependentColumns.getMaxNumberOfValues();
        int aBiggestMinRowNumbers = getBiggestMinRowNumbersOfDependentColumns();

        int aRequiredRows = Math.max(aBiggestMinRowNumbers, aMaxNumberOfDependentValues);

        for (Column aColumn : aDependentColumns) {
            aColumn.generateAdditionalExtendedDistribution(aRequiredRows);
        }
    }

    private int getBiggestMinRowNumbersOfDependentColumns() {
        int result = 0;

        for (Column aColumn : aDependentColumns) {
            int aMinRowNumbers = aColumn.getOwner().getMinRowNumbers();
            if (aMinRowNumbers > result) {
                result = aMinRowNumbers;
            }
        }

        return result;
    }
    private void generateReferencedData() throws Exception {
        openStoragesForCompositeDependency();

        int aMaxNumberOfDependentValues = aDependentColumns.getMaxNumberOfValues();
        this.getDistributor().setupRange(aMaxNumberOfDependentValues);

        for (int i = 0; i < aMaxNumberOfDependentValues; i++) {
            ArrayList<ValueSeriaInfo> aDependentValues = aDependentColumns.readNextValueSeriaInfo();
            ArrayList<Integer> aIndexes = getIndexesFromSeriaInfos(aDependentValues);

            int aCompositeId = calculateCompositeId(aIndexes, aDependentColumns.getMaxNumberOfUniqueValues());
            int aCompositeIdDistribution = calculateCompositeIdDistribution(aUniqueValuesDataStorage.getNumberOfValues());
            ValueSeriaInfo aCompositeSeriaInfo = new ValueSeriaInfo(aCompositeId, aCompositeIdDistribution);

            distributeNewReferencedValues(aDependentValues, aCompositeSeriaInfo);
        }
        closeStoragesForCompositeDependency();
    }

    protected void closeStoragesForCompositeDependency() throws Exception {
        aUniqueValuesDataStorage.closeAfterAppend();
        aReferencedColumns.closeAfterAppend();
        aDependentColumns.closeAfterRead();
    }

    protected void openStoragesForCompositeDependency() throws Exception {
        aDependentColumns.openForRead();
        aReferencedColumns.openForAppend();
        aUniqueValuesDataStorage.openForAppend();
    }

    protected void distributeNewReferencedValues(ArrayList<ValueSeriaInfo> aDependentValues, ValueSeriaInfo aCompositeSeriaInfo) throws Exception {
        if (aUniqueValuesDataStorage.isSeriaOfValueWasNotDistributed(aCompositeSeriaInfo)) {
            for (int aIndex = 0; aIndex < aDependentValues.size(); aIndex++) {

                int aReferencedValue = aDependentValues.get(aIndex).getValue();
                int aReferencedValueCount = aCompositeSeriaInfo.getValueCount();

                ValueSeriaInfo aReferencedSeriaInfo = new ValueSeriaInfo(aReferencedValue, aReferencedValueCount);

                Column aReferencedColumn = aReferencedColumns.get(aIndex);
                aReferencedSeriaInfo = aReferencedColumn.appendNewUniqueValueSeriaInfo(aReferencedSeriaInfo);
                aReferencedColumn.appendValueSeriaInfo(aReferencedSeriaInfo);
            }
            aUniqueValuesDataStorage.appendNewValueSeriaInfo(aCompositeSeriaInfo);
        }
    }

    private ArrayList<Integer> getIndexesFromSeriaInfos(ArrayList<ValueSeriaInfo> aValueSeriaInfos) {
        ArrayList<Integer> aIndexes = new ArrayList<Integer>();
        for (ValueSeriaInfo aValueSeriaInfo : aValueSeriaInfos) {
            aIndexes.add(aValueSeriaInfo.getUniqueValueIndex());
        }
        return aIndexes;
    }

    protected int calculateCompositeId(ArrayList<Integer> aIndexes, int aBasis) {
        int aExponent = aIndexes.size() - 1;
        int result = 0;

        for (int aIndex : aIndexes) {
            result += aIndex * (int) Math.pow(aBasis, aExponent);
            aExponent--;
        }

        return result;
    }

    protected int calculateCompositeIdDistribution(int aValue) throws Exception {
        int result = this.getDistributor().getCountOfValue(aValue);
        if (result == 0) {
            result = 1;
        }
        return result;
    }

    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append("(").append(printDependentColumns()).append(")");
        result.append(" => ");
        result.append("(").append(printReferencedColumns()).append(")");
        return result.toString();
    }

    private String printDependentColumns() {
        StringBuilder result = new StringBuilder();
        for (Column aDependentColumn : aDependentColumns) {
            if (result.length() > 0) {
                result.append(",");
            }
            result.append(aDependentColumn);
        }
        return result.toString();
    }

    private String printReferencedColumns() {
        StringBuilder result = new StringBuilder();
        for (Column aReferencedColumn : aReferencedColumns) {
            if (result.length() > 0) {
                result.append(",");
            }
            result.append(aReferencedColumn);
        }
        return result.toString();
    }
}
